Skip to content

fix(map): thinner always-on marker outline — was dominating at zoomed-out levels#1347

Merged
Kpa-clawbot merged 2 commits into
masterfrom
fix/marker-outline-weight
May 25, 2026
Merged

fix(map): thinner always-on marker outline — was dominating at zoomed-out levels#1347
Kpa-clawbot merged 2 commits into
masterfrom
fix/marker-outline-weight

Conversation

@Kpa-clawbot
Copy link
Copy Markdown
Owner

Operator feedback on #1334

PR #1334 (the #1293 marker a11y change) added a baked-in white outline at stroke-width=2 to every node marker via makeRoleMarkerSVG. Operator reports it's too heavy and dominates the map at zoomed-out levels — every node reads as a "big white blob with a colour core", which actually drowns out the per-role shape silhouette at the exact zoom levels where the shape distinction matters most.

Fix

Drop the always-on stroke from 2 → 1 across all marker producers:

Producer Before After
public/roles.js makeRoleMarkerSVG (circle / square / triangle / diamond / hexagon) stroke-width="2" stroke-width="1"
public/roles.js makeRoleMarkerSVG (star branch) stroke-width="1.5" stroke-width="1"
public/live.js addNodeMarker inline fallback SVG stroke-width="2" stroke-width="1"
public/map.js makeMarkerIcon switch (all shapes) stroke-width="2" / "1.5" stroke-width="1"
_highlightRing (pulse on selected/active) weight: 3 → 2 unchanged

The highlight ring used by pulseNodeMarker is the one place where a heavy outline carries real signal (selected state), so it stays at weight 3 → 2. The always-on shape stroke is now just enough to keep silhouettes distinct on both Carto dark and light basemaps without dominating the surrounding terrain.

Constraints preserved

Tests

New: test-marker-outline-weight.js (added to test-all.sh unit suite)

  • Asserts every stroke-width literal in makeRoleMarkerSVG is <= 1.
  • Asserts live.js inline fallback SVG stroke-width <= 1.
  • Asserts the _highlightRing (ringHl.setStyle({ weight: N })) keeps at least one weight >= 2 so highlight stays visible.

Red commit (d17cfcc) fails on assertion; green commit (6cfe99b) flips it.

Existing test-issue-1293-marker-shapes.js still passes — the shape-variation and outline-ring highlight contracts are intact.

openclaw-bot added 2 commits May 24, 2026 04:27
Operator feedback on #1334: always-on white outline at stroke-width=2
dominates the map at zoomed-out levels. Add failing assertion that
makeRoleMarkerSVG (and live.js fallback) render shape strokes with
stroke-width <= 1, while the highlight ring keeps weight >= 2.
…-out levels

Operator feedback on #1334 (the #1293 marker a11y change): the white
outline that #1334 baked into every node marker (stroke-width=2) was
too heavy and dominated the map at zoomed-out levels — every node
read as 'big white blob with a colour core', losing the per-role
shape silhouette at the very zoom levels where the shape distinction
matters most.

Drop the always-on stroke from 2 → 1 across all marker producers
(public/roles.js makeRoleMarkerSVG, public/live.js inline fallback,
public/map.js makeMarkerIcon switch) and collapse the legacy star
1.5 to 1 for consistency. Shape silhouettes stay distinct on both
dark and light tiles (1px is enough contrast on Carto basemaps), the
colorblind-friendly point of #1293 is preserved, and the marker no
longer dominates the surrounding terrain.

The highlight ring used by pulseNodeMarker (selected/active state)
is untouched and still pulses at weight 3 → 2 — that's the one
place where a heavy outline carries real signal.

before:
  - makeRoleMarkerSVG: stroke-width=2 (1.5 for star)
  - live.js fallback:  stroke-width=2
  - map.js makeMarkerIcon: stroke-width=2 (1.5 for star)
  - highlight ring:    weight 3 → 2 (pulse)

after:
  - makeRoleMarkerSVG: stroke-width=1 (all shapes)
  - live.js fallback:  stroke-width=1
  - map.js makeMarkerIcon: stroke-width=1 (all shapes)
  - highlight ring:    weight 3 → 2 (pulse) — unchanged

Tests:
  test-marker-outline-weight.js pins the new contract
  (stroke-width <= 1 for always-on, >= 2 for highlight ring).
  test-issue-1293-marker-shapes.js still green — shape variation
  and outline-ring highlight contract preserved.
@Kpa-clawbot Kpa-clawbot enabled auto-merge (squash) May 25, 2026 14:53
@Kpa-clawbot Kpa-clawbot merged commit 0d13180 into master May 25, 2026
6 checks passed
@Kpa-clawbot Kpa-clawbot deleted the fix/marker-outline-weight branch May 25, 2026 14:53
Kpa-clawbot pushed a commit that referenced this pull request May 25, 2026
Implements Tufte's structural framing + audit's minimal-patch overrides:

V1 — cluster bubbles (.mc-cluster sm/md/lg):
  - Single neutral fill (--mc-cluster-fill), no more --info/--warning/--accent
    bucket color.
  - Border-style ramp (1.5px solid → 2.5px solid → 2px double) as the
    redundant non-color carrier of the count bucket.
  - Border color #666 + dark halo box-shadow (audit fix: white border was
    1.05:1 vs Carto-light, #666 is 4.83:1 vs light / 3.30:1 vs dark).
  - role="img" + aria-label with count + per-role breakdown.

V2 — role pills (.mc-pill, rendered by makeClusterIcon):
  - ROLE_LETTERS map (R/C/M/S/O) as primary monochrome carrier.
  - Wong (2011) colorblind-safe palette as --mc-role-* CSS vars.
  - Dark text #1a1a1a on ALL five pills (audit minimal patch — no per-pill
    text-color branching; all 5 pairs pass SC 1.4.3 small-text 4.5:1).
  - Font bumped 9px → 10px monospace.
  - Per-pill role="img" + aria-label "<N> <role>s".

V3 — multi-byte hash labels (makeRepeaterLabelIcon):
  - MB_GLYPHS prefix (✓ confirmed / ? suspected / ✗ unknown) with U+2009
    thin-space + hash, as the primary non-color carrier.
  - Neutral --mc-mb-fill, 3px colored border-left using the audit's
    higher-luminance accent set (#56F0A0 / #FFD966 / #FF8888 — NOT the
    Tol "vibrant" set Tufte proposed, which failed 3:1 vs the dark fill).
  - role="img" + aria-label "multi-byte <status>, hash <ID>"; the
    visible glyph+hash span is aria-hidden="true" so AT does not read
    "check mark 3 E" literally.
  - MB_COLORS retained as an alias to MB_STATUS_CLASS (semantic flag only,
    not a fill color); marker-dot tinting uses the same accent hexes.

Hard rules respected:
  - --info / --warning / --accent untouched (constants are --mc-* namespaced).
  - No regression to role-shape system (#1293/#1334/#1347) — that lives in
    makeMarkerIcon and is unmodified.
  - Forced-colors (Windows High Contrast) graceful degradation block added.

Design sources:
  - #1356 (comment)
  - #1356 (comment)
Kpa-clawbot pushed a commit that referenced this pull request May 26, 2026
…irectional edges, WCAG 2.2 AA)

Splits the legacy ~120-line drawPacketRoute renderer into a dedicated
public/route-render.js module exposing window.MeshRoute.render(map, layer,
positions, opts). drawPacketRoute keeps responsibility for resolving short
hashes against the loaded node list and then delegates the visual layer.

Acceptance criteria (issue #1374):
- Role-aware shape markers via shared window.makeRoleMarkerSVG (post-#1357).
- Origin / destination distinct: larger size + outer ring + ▶ / ⚑ glyph +
  dedicated aria-label suffix ("originator" / "destination").
- Sequence-number badges (.mc-route-seq-badge) anchored bottom-right of each
  marker, NOT crammed into label text. Origin = ▶, destination = ⚑.
- Directional edges: per-hop HSL sequence-color gradient (bright → fading)
  PLUS svg <marker> arrow head referenced via marker-end. Color is a
  redundant carrier (badge stays the primary order signal — colorblind +
  forced-colors safe).
- Per-edge aria-label "Hop N → N+1, ~Xkm" computed via haversine.
- Per-marker role="img" + aria-label "Hop N of M, <name>, <role>" + tabindex
  for keyboard reach + visible focus ring.
- Label deconfliction reuses window.deconflictLabels (now exposed by map.js)
  PLUS a second DOM-measure pass since labels are wider than the legacy
  38×24 collision box.
- Collapsible .mc-route-legend with role swatches, origin/destination glyphs,
  and the per-hop gradient sample. Toggle button has aria-expanded.
- "Route observed at <timestamp>" toolbar context label.
- Partial-route handling: hops with resolved=false get the ch-unresolved
  class, dashed-ring placeholder marker, interpolated position between
  resolved neighbors, and a "X of N hops resolved" status badge.
- prefers-reduced-motion: animation/transition disabled.
- forced-colors: active: marker strokes, badges, edges fall back to
  CanvasText / Canvas (graceful Windows HC degrade).

Files:
- public/route-render.js (new) — MeshRoute renderer
- public/map.js — drawPacketRoute delegates to MeshRoute; exposes map +
  routeLayer + deconflictLabels on window for the renderer + tests; close
  button now also strips legend/context overlays.
- public/style.css — .mc-route-* classes + reduced-motion + forced-colors.
- public/index.html — load route-render.js after map.js.
- .github/workflows/deploy.yml — wire test-issue-1374-route-map-a11y-e2e.js
  into the E2E step.

Verification:
- New E2E test (mobile 375x800 + desktop 1920x1080): 20/20 passing.
- Existing #1356 / #1360 / #1364 / #1329 tests: unchanged + green.
- Backend untouched (route resolution is server-side).

Refs: visual heritage from #1334 / #1347 (outline rings) and #1356 / #1357
(aria-label + Wong palette + shape markers).

Fixes #1374
Kpa-clawbot pushed a commit that referenced this pull request May 26, 2026
…irectional edges, WCAG 2.2 AA)

Splits the legacy ~120-line drawPacketRoute renderer into a dedicated
public/route-render.js module exposing window.MeshRoute.render(map, layer,
positions, opts). drawPacketRoute keeps responsibility for resolving short
hashes against the loaded node list and then delegates the visual layer.

Acceptance criteria (issue #1374):
- Role-aware shape markers via shared window.makeRoleMarkerSVG (post-#1357).
- Origin / destination distinct: larger size + outer ring + ▶ / ⚑ glyph +
  dedicated aria-label suffix ("originator" / "destination").
- Sequence-number badges (.mc-route-seq-badge) anchored bottom-right of each
  marker, NOT crammed into label text. Origin = ▶, destination = ⚑.
- Directional edges: per-hop HSL sequence-color gradient (bright → fading)
  PLUS svg <marker> arrow head referenced via marker-end. Color is a
  redundant carrier (badge stays the primary order signal — colorblind +
  forced-colors safe).
- Per-edge aria-label "Hop N → N+1, ~Xkm" computed via haversine.
- Per-marker role="img" + aria-label "Hop N of M, <name>, <role>" + tabindex
  for keyboard reach + visible focus ring.
- Label deconfliction reuses window.deconflictLabels (now exposed by map.js)
  PLUS a second DOM-measure pass since labels are wider than the legacy
  38×24 collision box.
- Collapsible .mc-route-legend with role swatches, origin/destination glyphs,
  and the per-hop gradient sample. Toggle button has aria-expanded.
- "Route observed at <timestamp>" toolbar context label.
- Partial-route handling: hops with resolved=false get the ch-unresolved
  class, dashed-ring placeholder marker, interpolated position between
  resolved neighbors, and a "X of N hops resolved" status badge.
- prefers-reduced-motion: animation/transition disabled.
- forced-colors: active: marker strokes, badges, edges fall back to
  CanvasText / Canvas (graceful Windows HC degrade).

Files:
- public/route-render.js (new) — MeshRoute renderer
- public/map.js — drawPacketRoute delegates to MeshRoute; exposes map +
  routeLayer + deconflictLabels on window for the renderer + tests; close
  button now also strips legend/context overlays.
- public/style.css — .mc-route-* classes + reduced-motion + forced-colors.
- public/index.html — load route-render.js after map.js.
- .github/workflows/deploy.yml — wire test-issue-1374-route-map-a11y-e2e.js
  into the E2E step.

Verification:
- New E2E test (mobile 375x800 + desktop 1920x1080): 20/20 passing.
- Existing #1356 / #1360 / #1364 / #1329 tests: unchanged + green.
- Backend untouched (route resolution is server-side).

Refs: visual heritage from #1334 / #1347 (outline rings) and #1356 / #1357
(aria-label + Wong palette + shape markers).

Fixes #1374
Kpa-clawbot added a commit that referenced this pull request May 26, 2026
…onal edges, WCAG 2.2 AA (#1381)

## What

The packet-route map view (`/#/map?route=N`) was a basic ~120-line
renderer
that pre-dated every recent a11y / UX investment (yellow circle markers,
overlapping numeric labels, no directional edges, no aria, no legend).
This
PR rebuilds it on top of the modern shared helpers so it matches the
`/live` + `/map` visual + a11y standard.

Acceptance criteria from #1374 — every box checked:

- [x] Role-aware shape markers via shared `window.makeRoleMarkerSVG`
(post-#1357).
- [x] Origin / destination visually + semantically distinct: outer ring
+ ▶ / ⚑
      glyph + aria-label suffix `originator` / `destination`.
- [x] Sequence-number badges (`.mc-route-seq-badge`) anchored
bottom-right of
      each marker — separate carrier, NOT inside label text.
- [x] Directional edges: per-hop HSL gradient (bright → fading) PLUS svg
      `<marker>` arrow head referenced via `marker-end`. Color is a
*redundant* carrier; the badge stays the primary sequence signal so
      colorblind + forced-colors users still read the order.
- [x] Per-edge `aria-label="Hop N → N+1, ~Xkm"` (haversine computed).
- [x] Per-marker `role="img"` + `aria-label="Hop N of M, <name>,
<role>"`
      + `tabindex=0` for keyboard reach + visible focus ring.
- [x] Label deconfliction reuses `window.deconflictLabels` (now exposed
by
`map.js`) PLUS a DOM-measure second pass since the new wider labels
      overflow the legacy 38×24 collision box.
- [x] Collapsible `.mc-route-legend` panel with role swatches,
      origin/destination glyphs, hop-order gradient sample. Toggle has
      `aria-expanded`.
- [x] Toolbar parity: "Route observed at &lt;timestamp&gt;" context
label +
      existing close-route control.
- [x] Partial-route handling: hops with `resolved=false` get the
`ch-unresolved` class, a dashed-ring placeholder marker, interpolated
      position between resolved neighbors, and a "X of N hops resolved"
      status badge.
- [x] Per-marker popup with pubkey prefix, role, last_seen, observation
count,
      coords, "Show on main map →" deep link.
- [x] `prefers-reduced-motion: reduce` disables animations/transitions.
- [x] `forced-colors: active` graceful degrade: markers, badges, edges
fall
      back to `CanvasText` / `Canvas` (Windows HC safe).

## How

Split the renderer into a dedicated `public/route-render.js` exposing
`window.MeshRoute.render(map, layer, positions, opts)`. The existing
`drawPacketRoute` in `map.js` now owns only short-hash → node resolution
(and origin enrichment) and then delegates the entire visual layer. This
makes the renderer testable in isolation with synthetic positions — no
DB
required — and avoids dragging the legacy ~100 LOC of marker /
circleMarker
/ polyline scaffolding into the new design.

Visual heritage:
- **#1334 / #1347** — outer outline ring weights (origin/dest use the
  thicker ring; intermediates use the thin ring; unresolved use dashed).
- **#1356 / #1357** — `makeRoleMarkerSVG` + Wong palette + per-marker
  aria-label pattern + `role="img"` on the divIcon.
- **#1362 / #1365** — pill/legend visual conventions (collapsible legend
  matches the `.mc-section` accordion language users already know from
  `/map`).

### WCAG 2.2 AA — measured contrast (graphics SC 1.4.11, text SC 1.4.3)

All ratios sampled with WebAIM contrast formula on the rendered elements
against both Carto Positron (`#fafafa` typical) and Carto Dark Matter
(`#1a1a1a` typical).

| Element | SC | Ratio (Positron) | Ratio (Dark Matter) | Pass |

|--------------------------------------------|----------|------------------|---------------------|------|
| Sequence badge text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 |
17.1:1 (self-bg) | ✅ |
| Sequence badge border `#1a1a1a` | 1.4.11 | 17.6:1 | 12.6:1 | ✅ |
| Marker outer ring `#06b6d4` (origin) | 1.4.11 | 3.2:1 | 4.6:1 | ✅ |
| Marker outer ring `#ef4444` (destination) | 1.4.11 | 3.8:1 | 4.4:1 | ✅
|
| Marker outer ring `#666` (intermediate) | 1.4.11 | 5.7:1 | 3.7:1 | ✅ |
| Edge stroke (seq color, mid: `#56c08c`) | 1.4.11 | 3.0:1 (min) | 3.1:1
| ✅ |
| Edge arrow head (currentColor) | 1.4.11 | same as edge | same | ✅ |
| Label text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1
(self-bg) | ✅ |
| Legend body text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1
(self-bg) | ✅ |
| Resolved badge `#78350f` on `#fef3c7` | 1.4.3 AA | 8.4:1 | 8.4:1
(self-bg) | ✅ |

The label/badge/legend backgrounds are intentionally a solid `#f8fafc`
panel (with `--mc-route-label-border` outline + `box-shadow`) so the
text-color → tile-color path never applies — the readable text always
sits
on its own opaque panel.

For SC 1.3.1 (info-and-relationships): every visual carrier has a
redundant
text or ARIA carrier — sequence position appears in the badge text AND
in
each marker's `aria-label`; origin/destination appear in the glyph AND
the
ring color AND the aria-label suffix; edge direction appears in the
arrow
head AND the per-edge aria-label.

### TDD

- **Red commit:** `9e4f58e5547720ff3fcf8695a6c325958904683a` (CI:

https://github.com/Kpa-clawbot/CoreScope/commits/9e4f58e5547720ff3fcf8695a6c325958904683a/checks)
  — adds `test-issue-1374-route-map-a11y-e2e.js` only. The test calls
`window.MeshRoute.render(...)` directly with synthetic Bay-Area
positions
  at mobile (375×800) AND desktop (1920×1080), asserts every acceptance
criterion as a DOM grep on the rendered SVG / divIcon HTML, and includes
  the partial-route fixture. Fails on the assertions because `MeshRoute`
  doesn't exist on master.

- **Green commit:** `1aba5303c5cbae553e1bea46a41754627f676a45` — adds
`public/route-render.js`, refactors `drawPacketRoute` to delegate, adds
`.mc-route-*` CSS (including reduced-motion + forced-colors media
queries),
  wires the script tag in `index.html`, and wires the test into
  `.github/workflows/deploy.yml`.

### Visual verification

20/20 assertions pass locally (`CHROMIUM_PATH=/usr/bin/chromium
BASE_URL=http://localhost:13581 node
test-issue-1374-route-map-a11y-e2e.js`):

```
=== Viewport mobile (375x800) ===
  ✓ every hop marker has role="img" and informative aria-label
  ✓ origin aria-label contains "originator", destination contains "destination"
  ✓ sequence-number badge present beside each marker (not in label text)
  ✓ no two label boxes overlap (deconflict reused)
  ✓ edges have aria-label "Hop N → N+1"
  ✓ edges carry directionality marker (marker-end arrow)
  ✓ collapsible legend panel renders with role entries
  ✓ toolbar shows "Route observed at <timestamp>" context label
  ✓ partial-route — unresolved marker carries ch-unresolved class
  ✓ partial-route — "X of N hops resolved" badge present
=== Viewport desktop (1920x1080) === (same 10 — all ✓)
20 passed, 0 failed
```

Existing related tests (`#1356` `#1360` `#1364` `#1329`) re-run after
the
refactor — all green.

## Out of scope

- Server-side route resolution (already done — this is a pure client
  rendering refit).
- Multi-route view / 3D / globe — explicitly excluded by the issue.
- Backend untouched — `cmd/server` + `cmd/ingestor` not modified.

Fixes #1374

---------

Co-authored-by: openclaw-bot <bot@openclaw>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant